// Copyright 2009 Google Inc. All Rights Reserved.

/**
 * @fileoverview API library for accessing XML in the ECMA E4X style.
 *
 * http://en.wikipedia.org/wiki/ECMAScript_for_XML
 *
 * This initial implementation supports: 1) child nodes as properties;
 * 2) attribute; 3) text(), single mixed text value only; 4) descendants().
 *
 * @author wenboz@google.com (Wenbo Zhu)
 */

/**
 * Namespace for the E4X library.
 */
var e4x = {};

/**
 * Sub namespace for utility functions.
 */
e4x.util = {};

/**
 * Enumerations of common DOM node types.
 * @enum {number}
 */
e4x.NodeType = {
  ELEMENT: 1,
  TEXT: 3
};

/**
 * Exposes E4X like API for accessing the XML DOM data. Supported APIs include
 * 1) attributes, properties named as "@attrName"; 2) mixed text node, function
 * named as "text"; 3) child elements, properties named as element names, and
 * nested elements are supported and the property value will be a nested object,
 * and the text value of leaf elements is accessed as property value; 4) query-
 * by-element-name is supported, function named as "descendants"; 5) the local
 * node name, function named as "localName".
 *
 * If supported, the API syntax will be exactly what's defined under E4X.
 *
 * @param {string} xml The raw XML text or a DOM instance.
 * @constructor
 */
e4x.Xml = function(xml) {

  var node = e4x.util.parseXML_(xml);

  // Process any attributes.
  if (node.nodeType == e4x.NodeType.ELEMENT) {
    for (var i = 0, attribute; attribute = node.attributes[i]; i++) {
      this['@' + attribute.nodeName] = attribute.nodeValue;
    }
  }

  // Any mixed test node value.
  var textNode = {
    value: ''
  };

  // Note that all the supported E4X built-in APIs, such as localName, text,
  // and descendants are defined directly as closure (as opposed to with
  // prototype) to avoid introducing any private properties, which may
  // potentially collide with XML node names.

  // Process child nodes (recursively) and (mixed) test node.
  this.processNestedNodes_(node, textNode);

  /**
   * Gets the local name of the original XML node. In E4X, this is defined as a
   * built-in function.
   * @return {string} The local (Node) name of the current instance.
   */
  this.localName = function() {
    return node.nodeName;
  };

  /**
   * Gets the content of any mixed text node of the original XML node.
   * In E4X, this is defined as a built-in function.
   * @return {string} The text node  content of the current instance.
   */
  this.text = function() {
    return textNode.value;
  };
};

/**
 * Traverse (recursively) the nested child nodes and populates the target
 * E4X object instance. Used by the constructor.
 * @param {!Document} node The DOM instance.
 * @param {!Object} textNode To capture the text node value.
 * @private
 */
e4x.Xml.prototype.processNestedNodes_ = function(node, textNode) {
  var hasChildElement = false;
  if (node.hasChildNodes()) {
    for (var i = 0, childNode; childNode = node.childNodes[i]; i++) {
      if (childNode.nodeType == e4x.NodeType.TEXT) { // Single text only.
        textNode.value = childNode.nodeValue;
      } else if (childNode.nodeType == e4x.NodeType.ELEMENT) {
        var childObject;
        if (!this[childNode.nodeName]) {
          childObject = new e4x.Xml(childNode);
          // Leaf node as String value.
          // TODO(wenboz): if there exist attributes, use String() instead
          this[childNode.nodeName] =
              childObject.descendants ? childObject : childObject.text();
        } else {
          // Safe isArray() is expensive, and this serves as a short-path
          // (sufficient in this context).
          if (typeof this[childNode.nodeName].push != 'function') {
            var existingElement = this[childNode.nodeName];
            this[childNode.nodeName] = [];
            this[childNode.nodeName].push(existingElement);
          }
          childObject = new e4x.Xml(childNode);
          // Leaf node as String value.
          this[childNode.nodeName].push(
              childObject.descendants ? childObject : childObject.text());
        }
        hasChildElement = true;
      }
      // No other node types supported yet.
    }
  }

  // E4X uses "..nodeName" or "descendants('nodeName')"
  // for accessing descendants.
  if (hasChildElement) {
    this.descendants = function(childName) {
      return e4x.descendants(this, childName);
    };
  }
};

/**
 * None E4X function that serializes the object to XML string.
 * @return {string} The content as XML string.
 */
e4x.Xml.prototype.toXml = function() {
  return e4x.toXml(this);
};

/**
 * Does a depth-first search to find the FIRST child element(s) under the given
 * name.
 *
 * TODO(wenboz): check the spec to see if only the FIRST? Also, a breadth-first
 * search may be more efficient.
 *
 * @param {!e4x.Xml} e4xObject The parent E4X object instance.
 * @param {string} name The child element name.
 * @return {Object|Array} Either an E4X Object or an array of E4X Object.
 */
e4x.descendants = function(e4xObject, name) {
  if (e4xObject.descendants) {
    if (e4xObject[name]) {
      return e4xObject[name];
    } else {
      for (var child in e4xObject) {
        var childValue = e4xObject[child];
        if (typeof childValue != 'function') {
          var result = e4x.descendants(e4xObject[child], name);
          if (result) {
            return result;
          }
        }
      }
    }
  }
  return null;
};

/**
 * Writes the content of an e4xObject to XML string.
 *
 * TODO(wenboz): print format control, or use DOM API to generate XML.
 * TODO(wenboz): for now, we assume property value has already
 * been escaped (properly).
 *
 * @param {!e4x.Xml} e4xObject Represents a DOM instance (under the e4x.* API).
 * @param {string} opt_localName Optional, and when present, the top-level node
 *     will be named as such. Otherwise, the content may or may not be
 *     a single-node XML fragment.
 * @return {string} The XML string.
 */
e4x.toXml = function(e4xObject, opt_localName) {
  // TODO(wenboz): Better filtering?
  if (typeof e4xObject == 'object') {
    var result = [];

    // Scan the data.
    var xmlData = {
      attributes: [],
      childNodeContentList: [],
      childNodeNameList: [],
      nodeText: null
    };
    e4x.util.scanObject_(e4xObject, xmlData);

    // Print the XML.
    // 0. root node;
    var nodeName;
    if (opt_localName) {
      nodeName = opt_localName;
    } else if (e4xObject.localName) {
      nodeName = e4xObject.localName();
    }
    if (nodeName) {
      result.push('<', nodeName);
      // 1. attributes;
      for (var i = 0; i < xmlData.attributes.length; i++) {
        var attribute = xmlData.attributes[i];
        result.push(' ' + attribute);
      }
      if (nodeName) {
        result.push('>');
      }
    }

    // 2. child nodes;
    for (var i = 0; i < xmlData.childNodeContentList.length; i++) {
      var childNode = xmlData.childNodeContentList[i];
      if (typeof childNode == 'string') {
        result.push(xmlData.childNodeContentList[i]);
      } else if (typeof childNode == 'object') {
        result.push(e4x.toXml(childNode, xmlData.childNodeNameList[i]));
      }
    }

    // 3. mixed text node.
    if (xmlData.nodeText) {
      result.push(xmlData.nodeText);
    }

    if (nodeName) {
      result.push('</', nodeName, '>');
    }

    return result.join('');
  } else {
    throw 'invalid XML document';
  }
};

/**
 * Scans an E4X Object instance and identifies attributes, childNodes and
 * any (mixed) node text.
 *
 * @param {!e4x.Xml} e4xObject Represents a DOM instance (under the e4x.* API).
 * @param {!Object} xmlData Represents the collected XML data, such as
 *     attributes, childNodes, (mixed) node text.
 * @private
 */
e4x.util.scanObject_ = function(e4xObject, xmlData) {
  var attributes = xmlData.attributes;
  var childNodeContentList = xmlData.childNodeContentList;
  var childNodeNameList = xmlData.childNodeNameList;

  for (var property in e4xObject) {
    var propertyValue = e4xObject[property];
    if (typeof propertyValue == 'function') {
      continue;
    }
    if (property.charAt(0) == '@') {  // An attribute.
      attributes.push(property.slice(1) + '="' + propertyValue + '"');
    } else if (typeof propertyValue == 'string'){  // Leaf child node.
      childNodeContentList.push('<', property, '>', propertyValue,
          '</', property, '>');
      childNodeNameList.push(property);
    } else if (e4x.util.isArray(propertyValue)) {
      // A list of nested nodes of the same node name.
      for (var i = 0; i < propertyValue.length; i++) {
        var itemValue = propertyValue[i];
        if (typeof itemValue == 'string'){  // Leaf child node.
          childNodeContentList.push('<', property, '>', itemValue,
              '</', property, '>');
        } else if (typeof itemValue == 'object') {    // A nested node.
          childNodeContentList.push(itemValue);
        }
        childNodeNameList.push(property);
      }
    } else if (typeof propertyValue == 'object') {    // A nested node.
      childNodeContentList.push(propertyValue);
      childNodeNameList.push(property);
    }
    // Silently ignore everything else.
  }

  // Mixed test node (for a nested node).
  if (typeof e4xObject.text == 'function') {
    xmlData.nodeText = e4xObject.text();
  }
};

/**
 * Checks the parameter type and do XML parsing as needed.
 * @param {string} xml The raw XML text or a DOM instance.
 * @return {Document} The DOM instance that represents the XML.
 * @private
 */
e4x.util.parseXML_ = function(xml) {
  if (typeof xml == 'string') {
    if (window.DOMParser) {
      var parser = new DOMParser();
      dom = parser.parseFromString(xml, 'text/xml');
    } else {
      var doc = new ActiveXObject('Microsoft.XMLDOM');
      doc.async = false;
      doc.loadXML(xml);
      dom = doc;
    }
    return e4x.util.getFirstElementChild(dom);
  } else if (typeof xml == 'object' && xml.childNodes) {
    // Valid Document instance.
    // TODO(wenboz): make it more robust.
    return xml;
  } else {
    throw 'invalid XML input';
  }
};

/**
 * Finds the first element, i.e. the document element. This is more stable than
 * calling lastChilid, which may be comment e.g.
 * @param {!Document} dom The document instance.
 * @return {Element} The document element.
 */
e4x.util.getFirstElementChild = function(dom) {
  for (var i = 0, childNode; childNode = dom.childNodes[i]; i++) {
    if (childNode.nodeType == e4x.NodeType.ELEMENT) {
      return childNode;
    }
  }
  throw 'invalid XML document';
};

/**
 * Test if a value is an Array.
 *
 * @param {Object} a An object to be tested.
 * @return {boolean} True if the object is an Array.
 */
e4x.util.isArray = function(a) {
  return (a instanceof Array) || typeof a == 'object' &&
      typeof a.join == 'function' && typeof a.reverse == 'function';
};
